/**
 * Copyright (c) 2019 Coder League
 * All rights reserved.
 *
 * File：AbstractInterfaceService.java
 * History:
 *         2019年5月28日: Initially created, Chrise.
 */
package club.coderleague.ilsp.service.interfaces;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.codec.binary.Base64;
import org.apache.rocketmq.common.message.Message;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import club.coderleague.ilsp.common.config.properties.PaySettings;
import club.coderleague.ilsp.common.domain.beans.PayOrder;
import club.coderleague.ilsp.common.domain.beans.PaymentInterfaceResponse;
import club.coderleague.ilsp.common.domain.beans.RefundOrder;
import club.coderleague.ilsp.common.domain.beans.UserSession;
import club.coderleague.ilsp.common.domain.beans.WebAuthContext;
import club.coderleague.ilsp.common.domain.enums.EntityState;
import club.coderleague.ilsp.common.domain.enums.OrderPaymentMode;
import club.coderleague.ilsp.common.domain.enums.OrderPaymentState;
import club.coderleague.ilsp.common.domain.enums.OrderType;
import club.coderleague.ilsp.common.rocketmq.consumer.ApiRefundConsumer;
import club.coderleague.ilsp.common.rocketmq.producer.ApiRefundProducer;
import club.coderleague.ilsp.dao.OrdersDao;
import club.coderleague.ilsp.entities.Orders;
import club.coderleague.ilsp.service.AbstractService;
import club.coderleague.ilsp.service.bind.BoundService;
import club.coderleague.ilsp.util.ApplicationContextUtil;

/**
 * 接口服务基类。
 * @author Chrise
 */
public abstract class AbstractInterfaceService extends AbstractService {
	protected static final String AUTH_STATE_KEY = "auth_state";
	protected static final String KEY_PAY_API = "pay_api";
	private static final String AUTH_CODE_KEY = "auth_code";
	private static final String BIND_REQUEST_URL = "/mobile/bind.xhtml";
	private static final String CONFIRM_REQUEST_URL = "/mobile/member/order/confirm.xhtml?api=%s&order=%s";
	private static final String MEMBER_REQUEST_URL = "/mobile/member/main.xhtml";
	private static final String CUSTOMER_REQUEST_URL = "/mobile/customer/main.xhtml";
	private static final String MERCHANT_REQUEST_URL = "/mobile/merchant/main.xhtml";
	private static final Logger LOGGER = LoggerFactory.getLogger(AbstractInterfaceService.class);
	
	@Autowired
	private PaySettings pSettings;
	@Autowired
	private BoundService boundService;
	@Autowired
	private OrdersDao ordersDao;
	@Autowired
	private ThreadPoolTaskScheduler taskScheduler;
	@Autowired
	private ApiRefundProducer refundProducer;
	
	/**
	 * 授权成功事件。
	 * @author Chrise 2019年5月31日
	 * @param openId 开放标识。
	 * @param session 会话对象。
	 * @return 重定向地址。
	 */
	public String onAuthSuccess(String openId, HttpSession session) {
		try {
			// 获取网页授权上下文
			WebAuthContext context = (WebAuthContext)session.getAttribute(WebAuthContext.SESSION_KEY);
			if (context == null) return null;
			
			// 生成重定向地址
			String domain = getDomain();
			String redirect = domain + this.genRedirectUrl(context);
			
			// 检查开放标识是否已绑定
			UserSession us = this.boundService.execCheckBound(context.getScene(), context.getAuthor(), openId);
			if (us == null) {
				// 缓存开放标识
				context.setOpenid(openId);
				context.setRedirect(redirect);
				session.setAttribute(WebAuthContext.SESSION_KEY, context);
				return domain + BIND_REQUEST_URL;
			}
			
			// 切换会话
			session.removeAttribute(WebAuthContext.SESSION_KEY);
			session.setAttribute(UserSession.SESSION_KEY, us);
			
			return redirect;
		} catch (Exception e) {
			LOGGER.error(e.getMessage(), e);
		}
		
		return null;
	}
	
	/**
	 * 查询交易。
	 * @author Chrise 2019年8月16日
	 * @param order 订单对象。
	 * @return 交易结果，支付成功返回支付接口响应对象，否则返回null。
	 * @throws Exception 交易查询错误。
	 */
	public abstract PaymentInterfaceResponse queryTrade(Orders order) throws Exception;
	
	/**
	 * 申请退款。
	 * @author Chrise 2019年8月16日
	 * @param order 订单对象。
	 * @throws Exception 退款申请错误。
	 */
	public abstract void applyRefund(RefundOrder order) throws Exception;
	
	/**
	 * 查询退款。
	 * @author Chrise 2019年8月16日
	 * @param order 订单对象。
	 * @return 支付接口响应对象。
	 * @throws Exception 退款查询错误。
	 */
	public abstract PaymentInterfaceResponse queryRefund(Orders order) throws Exception;
	
	/**
	 * 获取支付订单。
	 * @author Chrise 2019年6月10日
	 * @param order 订单标识。
	 * @return 支付订单对象。
	 */
	protected PayOrder getPayOrder(long order) {
		// 获取、验证支付订单
		PayOrder po = this.ordersDao.queryPayOrder(order);
		if (!this.verifyPayOrderInner(po)) {
			LOGGER.error("The order [{}] is invalid with order object [{}].", order, po);
			return null;
		}
		
		// 设置支付订单属性
		if (OrderType.NORMAL.equalsValue(po.getOrdertype())) po.setOrdername(this.pSettings.getShoppingName());
		else if (OrderType.RECHARGE.equalsValue(po.getOrdertype())) po.setOrdername(this.pSettings.getRechargeName());
		
		return po;
	}
	
	/**
	 * 启动支付通知发送任务。
	 * @author Chrise 2019年6月20日
	 * @param url 通知地址。
	 * @param order 订单标识。
	 */
	protected void startPayNotifySendTask(String url, long order) {
		// 启动通知发送任务
		this.taskScheduler.schedule(new Runnable() {
			public void run() {
				sendPayNotify(url, String.valueOf(order));
			}
		}, new Date(System.currentTimeMillis() + 10L));
	}
	
	/**
	 * 验证退款订单。
	 * @author Chrise 2019年8月1日
	 * @param order 退款订单对象。
	 * @return 退款订单有效时返回true，否则返回false。
	 */
	protected boolean verifyRefundOrder(RefundOrder order) {
		// 验证退款订单
		if (!this.verifyRefundOrderInner(order)) {
			LOGGER.error("The order is invalid with order object [{}].", order);
			return false;
		}
		
		// 设置退款订单属性
		if (order.getPartialrefund()) order.setRefundtradeno(String.valueOf(order.getRefundid()));
		else order.setRefundtradeno(String.valueOf(order.getOrderid()));
		
		return true;
	}
	
	/**
	 * 启动退款通知发送任务。
	 * @author Chrise 2019年8月7日
	 * @param url 通知地址。
	 * @param order 订单标识。
	 * @param refund 退款标识。
	 */
	protected void startRefundNotifySendTask(String url, long order, String refund) {
		// 启动通知发送任务
		this.taskScheduler.schedule(new Runnable() {
			public void run() {
				sendRefundNotify(url, String.valueOf(order), refund);
			}
		}, new Date(System.currentTimeMillis() + 10L));
	}
	
	/**
	 * 发送退款消息。
	 * @author Chrise 2019年8月5日
	 * @param pir 支付接口响应对象。
	 */
	protected void sendRefundMessage(PaymentInterfaceResponse pir) {
		byte[] body = ApiRefundConsumer.genBody(pir);
		Message message = new Message(ApiRefundConsumer.TOPIC, body);
		
		this.refundProducer.send(message);
	}
	
	/**
	 * 获取域名。
	 * @author Chrise 2019年6月12日
	 * @return 域名。
	 */
	protected abstract String getDomain();
	
	/**
	 * 生成重定向地址。
	 * @author Chrise 2019年6月2日
	 * @param context 授权上下文。
	 * @return 重定向地址。
	 */
	private String genRedirectUrl(WebAuthContext context) {
		// 根据授权场景返回重定向地址
		switch (context.getScene()) {
			case SCAN_PAY:
				return String.format(CONFIRM_REQUEST_URL, context.getAuthor().getName(), context.getOrder());
			case MEMBER_MALL:
				return MEMBER_REQUEST_URL;
			case CUSTOMER_AREA:
				return CUSTOMER_REQUEST_URL;
			case MERCHANT_CONSOLE:
				return MERCHANT_REQUEST_URL;
			default:
				return null;
		}
	}
	
	/**
	 * 验证支付订单。
	 * @author Chrise 2019年6月20日
	 * @param order 支付订单对象。
	 * @return 支付订单有效时返回true，否则返回false。
	 */
	private boolean verifyPayOrderInner(PayOrder order) {
		// 订单为空
		if (order == null) return false;
		
		// 订单类型无效
		if (!OrderType.NORMAL.equalsValue(order.getOrdertype()) && !OrderType.RECHARGE.equalsValue(order.getOrdertype())) return false;
		
		// 支付总额无效
		if (order.getPaymenttotal() <= 0.0) return false;
		
		// 订单状态无效
		if (!EntityState.CONFIRMED.equalsValue(order.getOrderstate())) return false;
		
		// 支付状态无效
		if (!OrderPaymentState.UNPAID.equalsValue(order.getPaymentstate())) return false;
		
		return true;
	}
	
	/**
	 * 验证退款订单。
	 * @author Chrise 2019年8月1日
	 * @param order 退款订单对象。
	 * @return 退款订单有效时返回true，否则返回false。
	 */
	private boolean verifyRefundOrderInner(RefundOrder order) {
		// 退款总额无效
		if (order.getRefundtotal() <= 0.0) return false;
		
		// 订单状态无效
		if (!EntityState.CONFIRMED.equalsValue(order.getOrderstate()) && !EntityState.OTS.equalsValue(order.getOrderstate())) return false;
		
		// 订单类型无效
		if (!OrderType.NORMAL.equalsValue(order.getOrdertype())) return false;
		
		// 支付状态无效
		if (!OrderPaymentState.PAID.equalsValue(order.getPaymentstate())) return false;
		
		// 退款超时
		if (EntityState.OTS.equalsValue(order.getOrderstate()) && order.getRefundendtime().getTime() <= System.currentTimeMillis()) return false;
		
		return true;
	}
	
	/**
	 * 查询交易。
	 * @author Chrise 2019年8月16日
	 * @param order 订单标识。
	 * @return 交易结果，支付成功返回支付接口响应对象，否则返回null。
	 * @throws Exception 交易查询错误。
	 */
	public static PaymentInterfaceResponse queryTrade(long order) throws Exception {
		// 查询订单
		Orders o = findOrderDao().queryPartialOrder(order, false);
		if (o == null) throw new RuntimeException("The order not exists.");
		
		// 查询交易
		PaymentInterfaceResponse pir = findService(o.getPaymentmode()).queryTrade(o);
		return pir;
	}
	
	/**
	 * 申请退款。
	 * @author Chrise 2019年8月16日
	 * @param order 订单标识。
	 * @throws Exception 退款申请错误。
	 */
	public static void applyRefund(long order) throws Exception {
		// 查询订单
		RefundOrder o = findOrderDao().queryRefundOrder(order);
		if (o == null) throw new RuntimeException("The order not exists.");
		
		// 申请退款
		findService(o.getPaymentmode()).applyRefund(o);
	}
	
	/**
	 * 查询退款。
	 * @author Chrise 2019年8月16日
	 * @param order 订单标识。
	 * @return 支付接口响应对象。
	 * @throws Exception 退款查询错误。
	 */
	public static PaymentInterfaceResponse queryRefund(long order) throws Exception {
		// 查询订单
		Orders o = findOrderDao().queryPartialOrder(order, true);
		if (o == null) throw new RuntimeException("The order not exists.");
		
		// 查询退款
		PaymentInterfaceResponse pir = findService(o.getPaymentmode()).queryRefund(o);
		return pir;
	}
	
	/**
	 * 查找订单数据访问对象。
	 * @author Chrise 2019年8月16日
	 * @return 订单数据访问对象。
	 */
	private static OrdersDao findOrderDao() {
		ApplicationContext context = ApplicationContextUtil.getContext();
		OrdersDao dao = context.getBean(OrdersDao.class);
		return dao;
	}
	
	/**
	 * 查找支付接口服务。
	 * @author Chrise 2019年8月16日
	 * @param paymentmode 支付方式。
	 * @return 支付接口服务对象。
	 */
	private static AbstractInterfaceService findService(Integer paymentmode) {
		ApplicationContext context = ApplicationContextUtil.getContext();
		
		AbstractInterfaceService service = null;
		if (OrderPaymentMode.WECHAT.equalsValue(paymentmode)) {
			service = context.getBean(WeixinInterfaceService.class);
		} else if (OrderPaymentMode.ALIPAY.equalsValue(paymentmode)) {
			service = context.getBean(AlipayInterfaceService.class);
		} else throw new RuntimeException("Unknown payment interface.");
		
		return service;
	}
	
	/**
	 * 获取授权回调地址（模拟第三方平台）。
	 * @author Chrise 2019年5月28日
	 * @param redirect 重定向地址。
	 * @param state 状态。
	 * @param request 请求对象。
	 * @return 授权回调地址。
	 */
	public String getAuthCallbackUrl(String redirect, String state, HttpServletRequest request) {
		// 未启用模拟器时拒绝请求
		if (!isWebAuthSimulatorEnabled()) return null;
		
		// 获取授权代码
		String code = this.getCachedAuthCode(request);
		if (code == null) return null;
		
		// 构建授权回调地址
		return constructRedirectUrl(redirect, code, state);
	}
	
	/**
	 * 获取开放标识（模拟第三方平台）。
	 * @author Chrise 2019年5月28日
	 * @param code 授权代码。
	 * @return 开放标识。
	 */
	public Map<String, Object> getOpenId(String code) {
		Map<String, Object> response = new HashMap<>();
		
		// 启用模拟器时生成开放标识
		if (isWebAuthSimulatorEnabled()) {
			String openId = String.format("wx%s%s", new SimpleDateFormat("yy").format(new Date()), Base64.encodeBase64URLSafeString(code.getBytes()));
			constructOpenIdResponse(response, openId);
		}
		
		return response;
	}
	
	/**
	 * 缓存授权代码（模拟第三方平台）。
	 * @author Chrise 2019年5月28日
	 */
	protected void cacheAuthCode() {
		// 启用模拟器时缓存授权代码
		if (isWebAuthSimulatorEnabled()) {
			ServletRequestAttributes sra = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
			this.cacheAuthCode(sra.getRequest(), sra.getResponse());
		}
	}
	
	/**
	 * 检查网页授权模拟器是否启用（模拟第三方平台）。
	 * @author Chrise 2019年5月28日
	 * @return 网页授权模拟器启用时返回true，否则返回false。
	 */
	protected abstract boolean isWebAuthSimulatorEnabled();
	
	/**
	 * 构造重定向地址（模拟第三方平台）。
	 * @author Chrise 2019年5月28日
	 * @param redirect 重定向地址。
	 * @param code 授权代码。
	 * @param state 状态。
	 * @return 重定向地址。
	 */
	protected abstract String constructRedirectUrl(String redirect, String code, String state);
	
	/**
	 * 构造开放标识响应（模拟第三方平台）。
	 * @author Chrise 2019年5月28日
	 * @param response 响应对象。
	 * @param openId 开放标识。
	 */
	protected abstract void constructOpenIdResponse(Map<String, Object> response, String openId);
	
	/**
	 * 发送支付通知（模拟第三方平台）。
	 * @author Chrise 2019年6月21日
	 * @param url 通知地址。
	 * @param order 订单标识。
	 */
	protected abstract void sendPayNotify(String url, String order);
	
	/**
	 * 发送退款通知（模拟第三方平台）。
	 * @author Chrise 2019年8月7日
	 * @param url 通知地址。
	 * @param order 订单标识。
	 * @param refund 退款标识。
	 */
	protected abstract void sendRefundNotify(String url, String order, String refund);
	
	/**
	 * 缓存授权代码（模拟第三方平台）。
	 * @author Chrise 2019年5月28日
	 * @param request 请求对象。
	 * @param response 响应对象。
	 */
	private void cacheAuthCode(HttpServletRequest request, HttpServletResponse response) {
		// 获取已缓存的授权代码
		String value = this.getCachedAuthCode(request);
		if (value == null) value = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date());
		
		// 重新缓存授权代码
		Cookie cookie = new Cookie(AUTH_CODE_KEY, value);
		cookie.setMaxAge(10 * 365 * 24 * 60 * 60);
		cookie.setPath(request.getContextPath());
		response.addCookie(cookie);
	}
	
	/**
	 * 获取缓存的授权代码（模拟第三方平台）。
	 * @author Chrise 2019年5月28日
	 * @param request 请求对象。
	 * @return 授权代码。
	 */
	private String getCachedAuthCode(HttpServletRequest request) {
		// 读取COOKIE对象
		Cookie[] cookies = request.getCookies();
		if (cookies == null || cookies.length <= 0) return null;
		
		// 从缓存中查找授权代码
		for (Cookie cookie : cookies) {
			if (AUTH_CODE_KEY.equals(cookie.getName())) return cookie.getValue();
		}
		
		return null;
	}
}
